/*
 * Copyright 2002-2024 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.context.annotation;

import org.springframework.aop.TargetSource;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aot.generate.AccessControl;
import org.springframework.aot.generate.GeneratedClass;
import org.springframework.aot.generate.GeneratedMethod;
import org.springframework.aot.generate.GenerationContext;
import org.springframework.aot.hint.ExecutableMode;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.support.ClassHintUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.PropertyValues;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor;
import org.springframework.beans.factory.annotation.InjectionMetadata;
import org.springframework.beans.factory.aot.BeanRegistrationAotContribution;
import org.springframework.beans.factory.aot.BeanRegistrationCode;
import org.springframework.beans.factory.config.*;
import org.springframework.beans.factory.support.AutowireCandidateResolver;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.RegisteredBean;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.core.BridgeMethodResolver;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.javapoet.ClassName;
import org.springframework.javapoet.CodeBlock;
import org.springframework.jndi.support.SimpleJndiBeanFactory;
import org.springframework.lang.Nullable;
import org.springframework.util.*;

import java.beans.PropertyDescriptor;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

public class CommonAnnotationBeanPostProcessor
        extends InitDestroyAnnotationBeanPostProcessor
        implements InstantiationAwareBeanPostProcessor, BeanFactoryAware, Serializable {

    // Defensive reference to JNDI API for JDK 9+ (optional java.naming module)
    private static final boolean jndiPresent = ClassUtils.isPresent("javax.naming.InitialContext", CommonAnnotationBeanPostProcessor.class.getClassLoader());

    private static final Set<Class<? extends Annotation>> resourceAnnotationTypes = new LinkedHashSet<>(4);

    @Nullable
    private static final Class<? extends Annotation> jakartaResourceType;

    @Nullable
    private static final Class<? extends Annotation> javaxResourceType;

    @Nullable
    private static final Class<? extends Annotation> ejbAnnotationType;

    static {
        jakartaResourceType = loadAnnotationType("jakarta.annotation.Resource");
        if (jakartaResourceType != null) {
            resourceAnnotationTypes.add(jakartaResourceType);
        }

        javaxResourceType = loadAnnotationType("javax.annotation.Resource");
        if (javaxResourceType != null) {
            resourceAnnotationTypes.add(javaxResourceType);
        }

        ejbAnnotationType = loadAnnotationType("jakarta.ejb.EJB");
        if (ejbAnnotationType != null) {
            resourceAnnotationTypes.add(ejbAnnotationType);
        }
    }


    private final Set<String> ignoredResourceTypes = new HashSet<>(1);

    private boolean fallbackToDefaultTypeMatch = true;

    private boolean alwaysUseJndiLookup = false;

    @Nullable
    private transient BeanFactory jndiFactory;

    @Nullable
    private transient BeanFactory resourceFactory;

    @Nullable
    private transient BeanFactory beanFactory;

    @Nullable
    private transient StringValueResolver embeddedValueResolver;

    private final transient Map<String, InjectionMetadata> injectionMetadataCache = new ConcurrentHashMap<>(256);


    /**
     * Create a new CommonAnnotationBeanPostProcessor,
     * with the init and destroy annotation types set to
     * {@link jakarta.annotation.PostConstruct} and {@link jakarta.annotation.PreDestroy},
     * respectively.
     */
    public CommonAnnotationBeanPostProcessor() {
        setOrder(Ordered.LOWEST_PRECEDENCE - 3);

        // Jakarta EE 9 set of annotations in jakarta.annotation package
        addInitAnnotationType(loadAnnotationType("jakarta.annotation.PostConstruct"));
        addDestroyAnnotationType(loadAnnotationType("jakarta.annotation.PreDestroy"));

        // Tolerate legacy JSR-250 annotations in javax.annotation package
        addInitAnnotationType(loadAnnotationType("javax.annotation.PostConstruct"));
        addDestroyAnnotationType(loadAnnotationType("javax.annotation.PreDestroy"));

        // java.naming module present on JDK 9+?
        if (jndiPresent) {
            this.jndiFactory = new SimpleJndiBeanFactory();
        }
    }


    /**
     * Ignore the given resource type when resolving {@code @Resource} annotations.
     *
     * @param resourceType the resource type to ignore
     */
    public void ignoreResourceType(String resourceType) {
        Assert.notNull(resourceType, "Ignored resource type must not be null");
        this.ignoredResourceTypes.add(resourceType);
    }

    /**
     * Set whether to allow a fallback to a type match if no explicit name has been
     * specified. The default name (i.e. the field name or bean property name) will
     * still be checked first; if a bean of that name exists, it will be taken.
     * However, if no bean of that name exists, a by-type resolution of the
     * dependency will be attempted if this flag is "true".
     * <p>Default is "true". Switch this flag to "false" in order to enforce a
     * by-name lookup in all cases, throwing an exception in case of no name match.
     *
     * @see org.springframework.beans.factory.config.AutowireCapableBeanFactory#resolveDependency
     */
    public void setFallbackToDefaultTypeMatch(boolean fallbackToDefaultTypeMatch) {
        this.fallbackToDefaultTypeMatch = fallbackToDefaultTypeMatch;
    }

    /**
     * Set whether to always use JNDI lookups equivalent to standard Jakarta EE resource
     * injection, <b>even for {@code name} attributes and default names</b>.
     * <p>Default is "false": Resource names are used for Spring bean lookups in the
     * containing BeanFactory; only {@code mappedName} attributes point directly
     * into JNDI. Switch this flag to "true" for enforcing Jakarta EE style JNDI lookups
     * in any case, even for {@code name} attributes and default names.
     *
     * @see #setJndiFactory
     * @see #setResourceFactory
     */
    public void setAlwaysUseJndiLookup(boolean alwaysUseJndiLookup) {
        this.alwaysUseJndiLookup = alwaysUseJndiLookup;
    }

    /**
     * Specify the factory for objects to be injected into {@code @Resource} /
     * {@code @EJB} annotated fields and setter methods,
     * <b>for {@code mappedName} attributes that point directly into JNDI</b>.
     * This factory will also be used if "alwaysUseJndiLookup" is set to "true" in order
     * to enforce JNDI lookups even for {@code name} attributes and default names.
     * <p>The default is a {@link org.springframework.jndi.support.SimpleJndiBeanFactory}
     * for JNDI lookup behavior equivalent to standard Jakarta EE resource injection.
     *
     * @see #setResourceFactory
     * @see #setAlwaysUseJndiLookup
     */
    public void setJndiFactory(BeanFactory jndiFactory) {
        Assert.notNull(jndiFactory, "BeanFactory must not be null");
        this.jndiFactory = jndiFactory;
    }

    /**
     * Specify the factory for objects to be injected into {@code @Resource} /
     * {@code @EJB} annotated fields and setter methods,
     * <b>for {@code name} attributes and default names</b>.
     * <p>The default is the BeanFactory that this post-processor is defined in,
     * if any, looking up resource names as Spring bean names. Specify the resource
     * factory explicitly for programmatic usage of this post-processor.
     * <p>Specifying Spring's {@link org.springframework.jndi.support.SimpleJndiBeanFactory}
     * leads to JNDI lookup behavior equivalent to standard Jakarta EE resource injection,
     * even for {@code name} attributes and default names. This is the same behavior
     * that the "alwaysUseJndiLookup" flag enables.
     *
     * @see #setAlwaysUseJndiLookup
     */
    public void setResourceFactory(BeanFactory resourceFactory) {
        Assert.notNull(resourceFactory, "BeanFactory must not be null");
        this.resourceFactory = resourceFactory;
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) {
        Assert.notNull(beanFactory, "BeanFactory must not be null");
        this.beanFactory = beanFactory;
        if (this.resourceFactory == null) {
            this.resourceFactory = beanFactory;
        }
        if (beanFactory instanceof ConfigurableBeanFactory configurableBeanFactory) {
            this.embeddedValueResolver = new EmbeddedValueResolver(configurableBeanFactory);
        }
    }


    @Override
    public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition,
                                                Class<?> beanType,
                                                String beanName) {
        super.postProcessMergedBeanDefinition(beanDefinition, beanType, beanName);
        InjectionMetadata metadata = findResourceMetadata(beanName, beanType, null);
        metadata.checkConfigMembers(beanDefinition);
    }

    @Override
    public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) {
        BeanRegistrationAotContribution parentAotContribution = super.processAheadOfTime(registeredBean);
        Class<?> beanClass = registeredBean.getBeanClass();
        String beanName = registeredBean.getBeanName();
        RootBeanDefinition beanDefinition = registeredBean.getMergedBeanDefinition();
        InjectionMetadata metadata = findResourceMetadata(beanName, beanClass, beanDefinition.getPropertyValues());
        Collection<LookupElement> injectedElements = getInjectedElements(metadata, beanDefinition.getPropertyValues());
        if (!ObjectUtils.isEmpty(injectedElements)) {
            AotContribution aotContribution = new AotContribution(beanClass, injectedElements, getAutowireCandidateResolver(registeredBean));
            return BeanRegistrationAotContribution.concat(parentAotContribution, aotContribution);
        }
        return parentAotContribution;
    }

    @Nullable
    private AutowireCandidateResolver getAutowireCandidateResolver(RegisteredBean registeredBean) {
        if (registeredBean.getBeanFactory() instanceof DefaultListableBeanFactory lbf) {
            return lbf.getAutowireCandidateResolver();
        }
        return null;
    }

    @SuppressWarnings({"rawtypes", "unchecked"})
    private Collection<LookupElement> getInjectedElements(InjectionMetadata metadata,
                                                          PropertyValues propertyValues) {
        return (Collection) metadata.getInjectedElements(propertyValues);
    }

    @Override
    public void resetBeanDefinition(String beanName) {
        this.injectionMetadataCache.remove(beanName);
    }

    @Override
    public PropertyValues postProcessProperties(PropertyValues pvs,
                                                Object bean,
                                                String beanName) {
        InjectionMetadata metadata = findResourceMetadata(beanName, bean.getClass(), pvs);
        try {
            metadata.inject(bean, beanName, pvs);
        } catch (Throwable ex) {
            throw new BeanCreationException(beanName, "Injection of resource dependencies failed", ex);
        }
        return pvs;
    }

    /**
     * <em>Native</em> processing method for direct calls with an arbitrary target
     * instance, resolving all of its fields and methods which are annotated with
     * one of the supported 'resource' annotation types.
     *
     * @param bean the target instance to process
     * @throws BeanCreationException if resource injection failed
     * @since 6.1.3
     */
    public void processInjection(Object bean) throws BeanCreationException {
        Class<?> clazz = bean.getClass();
        InjectionMetadata metadata = findResourceMetadata(clazz.getName(), clazz, null);
        try {
            metadata.inject(bean, null, null);
        } catch (BeanCreationException ex) {
            throw ex;
        } catch (Throwable ex) {
            throw new BeanCreationException("Injection of resource dependencies failed for class [" + clazz + "]", ex);
        }
    }


    private InjectionMetadata findResourceMetadata(String beanName,
                                                   Class<?> clazz,
                                                   @Nullable PropertyValues pvs) {
        // Fall back to class name as cache key, for backwards compatibility with custom callers.
        String cacheKey = (StringUtils.hasLength(beanName) ? beanName : clazz.getName());
        // Quick check on the concurrent map first, with minimal locking.
        InjectionMetadata metadata = this.injectionMetadataCache.get(cacheKey);
        if (InjectionMetadata.needsRefresh(metadata, clazz)) {
            synchronized (this.injectionMetadataCache) {
                metadata = this.injectionMetadataCache.get(cacheKey);
                if (InjectionMetadata.needsRefresh(metadata, clazz)) {
                    if (metadata != null) {
                        metadata.clear(pvs);
                    }
                    metadata = buildResourceMetadata(clazz);
                    this.injectionMetadataCache.put(cacheKey, metadata);
                }
            }
        }
        return metadata;
    }

    private InjectionMetadata buildResourceMetadata(Class<?> clazz) {
        if (!AnnotationUtils.isCandidateClass(clazz, resourceAnnotationTypes)) {
            return InjectionMetadata.EMPTY;
        }

        List<InjectionMetadata.InjectedElement> elements = new ArrayList<>();
        Class<?> targetClass = clazz;

        do {
            final List<InjectionMetadata.InjectedElement> currElements = new ArrayList<>();

            Field[] fields = ReflectionUtils.getDeclaredFields(clazz);
            for (Field field : fields) {
                if (jakartaResourceType != null && field.isAnnotationPresent(jakartaResourceType)) {
                    if (Modifier.isStatic(field.getModifiers())) {
                        throw new IllegalStateException("@Resource annotation is not supported on static fields");
                    }
                    if (!this.ignoredResourceTypes.contains(field.getType().getName())) {
                        currElements.add(new ResourceElement(field, field, null));
                    }
                }
            }

            Method[] methods = ReflectionUtils.getDeclaredMethods(targetClass);
            for (Method method : methods) {
                Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
                if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) {
                    continue;
                }
                if (jakartaResourceType != null && bridgedMethod.isAnnotationPresent(jakartaResourceType)) {
                    if (method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) {
                        if (Modifier.isStatic(method.getModifiers())) {
                            throw new IllegalStateException("@Resource annotation is not supported on static methods");
                        }
                        Class<?>[] paramTypes = method.getParameterTypes();
                        if (paramTypes.length != 1) {
                            throw new IllegalStateException("@Resource annotation requires a single-arg method: " + method);
                        }
                        if (!this.ignoredResourceTypes.contains(paramTypes[0].getName())) {
                            PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz);
                            currElements.add(new ResourceElement(method, bridgedMethod, pd));
                        }
                    }
                }
            }

            elements.addAll(0, currElements);
            targetClass = targetClass.getSuperclass();
        } while (targetClass != null && targetClass != Object.class);

        return InjectionMetadata.forElements(elements, clazz);
    }

    /**
     * Obtain a lazily resolving resource proxy for the given name and type,
     * delegating to {@link #getResource} on demand once a method call comes in.
     *
     * @param element            the descriptor for the annotated field/method
     * @param requestingBeanName the name of the requesting bean
     * @return the resource object (never {@code null})
     * @see #getResource
     * @see Lazy
     * @since 4.2
     */
    protected Object buildLazyResourceProxy(LookupElement element,
                                            @Nullable String requestingBeanName) {
        TargetSource ts = new TargetSource() {
            @Override
            public Class<?> getTargetClass() {
                return element.lookupType;
            }

            @Override
            public Object getTarget() {
                return getResource(element, requestingBeanName);
            }
        };

        ProxyFactory pf = new ProxyFactory();
        pf.setTargetSource(ts);
        if (element.lookupType.isInterface()) {
            pf.addInterface(element.lookupType);
        }
        ClassLoader classLoader = (this.beanFactory instanceof ConfigurableBeanFactory configurableBeanFactory ? configurableBeanFactory.getBeanClassLoader() : null);
        return pf.getProxy(classLoader);
    }

    /**
     * Obtain the resource object for the given name and type.
     *
     * @param element            the descriptor for the annotated field/method
     * @param requestingBeanName the name of the requesting bean
     * @return the resource object (never {@code null})
     * @throws NoSuchBeanDefinitionException if no corresponding target resource found
     */
    protected Object getResource(LookupElement element,
                                 @Nullable String requestingBeanName) throws NoSuchBeanDefinitionException {

        // JNDI lookup to perform?
        String jndiName = null;
        if (StringUtils.hasLength(element.mappedName)) {
            jndiName = element.mappedName;
        } else if (this.alwaysUseJndiLookup) {
            jndiName = element.name;
        }
        if (jndiName != null) {
            if (this.jndiFactory == null) {
                throw new NoSuchBeanDefinitionException(element.lookupType, "No JNDI factory configured - specify the 'jndiFactory' property");
            }
            return this.jndiFactory.getBean(jndiName, element.lookupType);
        }

        // Regular resource autowiring
        if (this.resourceFactory == null) {
            throw new NoSuchBeanDefinitionException(element.lookupType, "No resource factory configured - specify the 'resourceFactory' property");
        }
        return autowireResource(this.resourceFactory, element, requestingBeanName);
    }

    /**
     * Obtain a resource object for the given name and type through autowiring
     * based on the given factory.
     *
     * @param factory            the factory to autowire against
     * @param element            the descriptor for the annotated field/method
     * @param requestingBeanName the name of the requesting bean
     * @return the resource object (never {@code null})
     * @throws NoSuchBeanDefinitionException if no corresponding target resource found
     */
    protected Object autowireResource(BeanFactory factory,
                                      LookupElement element,
                                      @Nullable String requestingBeanName) throws NoSuchBeanDefinitionException {

        Object resource;
        Set<String> autowiredBeanNames;
        String name = element.name;

        if (factory instanceof AutowireCapableBeanFactory autowireCapableBeanFactory) {
            if (this.fallbackToDefaultTypeMatch && element.isDefaultName && !factory.containsBean(name)) {
                autowiredBeanNames = new LinkedHashSet<>();
                resource = autowireCapableBeanFactory.resolveDependency(element.getDependencyDescriptor(), requestingBeanName, autowiredBeanNames, null);
                if (resource == null) {
                    throw new NoSuchBeanDefinitionException(element.getLookupType(), "No resolvable resource object");
                }
            } else {
                resource = autowireCapableBeanFactory.resolveBeanByName(name, element.getDependencyDescriptor());
                autowiredBeanNames = Collections.singleton(name);
            }
        } else {
            resource = factory.getBean(name, element.lookupType);
            autowiredBeanNames = Collections.singleton(name);
        }

        if (factory instanceof ConfigurableBeanFactory configurableBeanFactory) {
            for (String autowiredBeanName : autowiredBeanNames) {
                if (requestingBeanName != null && configurableBeanFactory.containsBean(autowiredBeanName)) {
                    configurableBeanFactory.registerDependentBean(autowiredBeanName, requestingBeanName);
                }
            }
        }

        return resource;
    }


    @SuppressWarnings("unchecked")
    @Nullable
    private static Class<? extends Annotation> loadAnnotationType(String name) {
        try {
            return (Class<? extends Annotation>) ClassUtils.forName(name, CommonAnnotationBeanPostProcessor.class.getClassLoader());
        } catch (ClassNotFoundException ex) {
            return null;
        }
    }


    /**
     * Class representing generic injection information about an annotated field
     * or setter method, supporting @Resource and related annotations.
     */
    protected abstract static class LookupElement
            extends InjectionMetadata.InjectedElement {

        protected String name = "";

        protected boolean isDefaultName = false;

        protected Class<?> lookupType = Object.class;

        @Nullable
        protected String mappedName;

        public LookupElement(Member member,
                             @Nullable PropertyDescriptor pd) {
            super(member, pd);
        }

        /**
         * Return the resource name for the lookup.
         */
        public final String getName() {
            return this.name;
        }

        /**
         * Return the desired type for the lookup.
         */
        public final Class<?> getLookupType() {
            return this.lookupType;
        }

        /**
         * Build a DependencyDescriptor for the underlying field/method.
         */
        public final DependencyDescriptor getDependencyDescriptor() {
            if (this.isField) {
                return new ResourceElementResolver.LookupDependencyDescriptor((Field) this.member, this.lookupType, isLazyLookup());
            } else {
                return new ResourceElementResolver.LookupDependencyDescriptor((Method) this.member, this.lookupType, isLazyLookup());
            }
        }

        /**
         * Determine whether this dependency is marked for lazy lookup.
         * The default is {@code false}.
         *
         * @since 6.1.2
         */
        boolean isLazyLookup() {
            return false;
        }
    }


    /**
     * Class representing injection information about an annotated field
     * or setter method, supporting the @Resource annotation.
     */
    private class ResourceElement
            extends LookupElement {

        private final boolean lazyLookup;

        public ResourceElement(Member member,
                               AnnotatedElement ae,
                               @Nullable PropertyDescriptor pd) {
            super(member, pd);
            jakarta.annotation.Resource resource = ae.getAnnotation(jakarta.annotation.Resource.class);
            String resourceName = resource.name();
            Class<?> resourceType = resource.type();
            this.isDefaultName = !StringUtils.hasLength(resourceName);
            if (this.isDefaultName) {
                resourceName = this.member.getName();
                if (this.member instanceof Method && resourceName.startsWith("set") && resourceName.length() > 3) {
                    resourceName = StringUtils.uncapitalizeAsProperty(resourceName.substring(3));
                }
            } else if (embeddedValueResolver != null) {
                resourceName = embeddedValueResolver.resolveStringValue(resourceName);
            }
            if (Object.class != resourceType) {
                checkResourceType(resourceType);
            } else {
                // No resource type specified... check field/method.
                resourceType = getResourceType();
            }
            this.name = (resourceName != null ? resourceName : "");
            this.lookupType = resourceType;
            String lookupValue = resource.lookup();
            this.mappedName = (StringUtils.hasLength(lookupValue) ? lookupValue : resource.mappedName());
            Lazy lazy = ae.getAnnotation(Lazy.class);
            this.lazyLookup = (lazy != null && lazy.value());
        }

        @Override
        protected Object getResourceToInject(Object target,
                                             @Nullable String requestingBeanName) {
            return (this.lazyLookup ? buildLazyResourceProxy(this, requestingBeanName) : getResource(this, requestingBeanName));
        }

        @Override
        boolean isLazyLookup() {
            return this.lazyLookup;
        }
    }

    /**
     * {@link BeanRegistrationAotContribution} to inject resources on fields and methods.
     */
    private static class AotContribution
            implements BeanRegistrationAotContribution {

        private static final String REGISTERED_BEAN_PARAMETER = "registeredBean";

        private static final String INSTANCE_PARAMETER = "instance";

        private final Class<?> target;

        private final Collection<LookupElement> lookupElements;

        @Nullable
        private final AutowireCandidateResolver candidateResolver;

        AotContribution(Class<?> target,
                        Collection<LookupElement> lookupElements,
                        @Nullable AutowireCandidateResolver candidateResolver) {

            this.target = target;
            this.lookupElements = lookupElements;
            this.candidateResolver = candidateResolver;
        }

        @Override
        public void applyTo(GenerationContext generationContext,
                            BeanRegistrationCode beanRegistrationCode) {
            GeneratedClass generatedClass = generationContext.getGeneratedClasses().addForFeatureComponent("ResourceAutowiring", this.target, type -> {
                type.addJavadoc("Resource autowiring for {@link $T}.", this.target);
                type.addModifiers(javax.lang.model.element.Modifier.PUBLIC);
            });
            GeneratedMethod generateMethod = generatedClass.getMethods().add("apply", method -> {
                method.addJavadoc("Apply resource autowiring.");
                method.addModifiers(javax.lang.model.element.Modifier.PUBLIC, javax.lang.model.element.Modifier.STATIC);
                method.addParameter(RegisteredBean.class, REGISTERED_BEAN_PARAMETER);
                method.addParameter(this.target, INSTANCE_PARAMETER);
                method.returns(this.target);
                method.addCode(generateMethodCode(generatedClass.getName(), generationContext.getRuntimeHints()));
            });
            beanRegistrationCode.addInstancePostProcessor(generateMethod.toMethodReference());

            registerHints(generationContext.getRuntimeHints());
        }

        private CodeBlock generateMethodCode(ClassName targetClassName,
                                             RuntimeHints hints) {
            CodeBlock.Builder code = CodeBlock.builder();
            for (LookupElement lookupElement : this.lookupElements) {
                code.addStatement(generateMethodStatementForElement(targetClassName, lookupElement, hints));
            }
            code.addStatement("return $L", INSTANCE_PARAMETER);
            return code.build();
        }

        private CodeBlock generateMethodStatementForElement(ClassName targetClassName,
                                                            LookupElement lookupElement,
                                                            RuntimeHints hints) {

            Member member = lookupElement.getMember();
            if (member instanceof Field field) {
                return generateMethodStatementForField(targetClassName, field, lookupElement, hints);
            }
            if (member instanceof Method method) {
                return generateMethodStatementForMethod(targetClassName, method, lookupElement, hints);
            }
            throw new IllegalStateException("Unsupported member type " + member.getClass().getName());
        }

        private CodeBlock generateMethodStatementForField(ClassName targetClassName,
                                                          Field field,
                                                          LookupElement lookupElement,
                                                          RuntimeHints hints) {

            hints.reflection().registerField(field);
            CodeBlock resolver = generateFieldResolverCode(field, lookupElement);
            AccessControl accessControl = AccessControl.forMember(field);
            if (!accessControl.isAccessibleFrom(targetClassName)) {
                return CodeBlock.of("$L.resolveAndSet($L, $L)", resolver, REGISTERED_BEAN_PARAMETER, INSTANCE_PARAMETER);
            }
            return CodeBlock.of("$L.$L = $L.resolve($L)", INSTANCE_PARAMETER, field.getName(), resolver, REGISTERED_BEAN_PARAMETER);
        }

        private CodeBlock generateFieldResolverCode(Field field,
                                                    LookupElement lookupElement) {
            if (lookupElement.isDefaultName) {
                return CodeBlock.of("$T.$L($S)", ResourceElementResolver.class, "forField", field.getName());
            } else {
                return CodeBlock.of("$T.$L($S, $S)", ResourceElementResolver.class, "forField", field.getName(), lookupElement.getName());
            }
        }

        private CodeBlock generateMethodStatementForMethod(ClassName targetClassName,
                                                           Method method,
                                                           LookupElement lookupElement,
                                                           RuntimeHints hints) {

            CodeBlock resolver = generateMethodResolverCode(method, lookupElement);
            AccessControl accessControl = AccessControl.forMember(method);
            if (!accessControl.isAccessibleFrom(targetClassName)) {
                hints.reflection().registerMethod(method, ExecutableMode.INVOKE);
                return CodeBlock.of("$L.resolveAndSet($L, $L)", resolver, REGISTERED_BEAN_PARAMETER, INSTANCE_PARAMETER);
            }
            hints.reflection().registerMethod(method, ExecutableMode.INTROSPECT);
            return CodeBlock.of("$L.$L($L.resolve($L))", INSTANCE_PARAMETER, method.getName(), resolver, REGISTERED_BEAN_PARAMETER);

        }

        private CodeBlock generateMethodResolverCode(Method method,
                                                     LookupElement lookupElement) {
            if (lookupElement.isDefaultName) {
                return CodeBlock.of("$T.$L($S, $T.class)", ResourceElementResolver.class, "forMethod", method.getName(), lookupElement.getLookupType());
            } else {
                return CodeBlock.of("$T.$L($S, $T.class, $S)", ResourceElementResolver.class, "forMethod", method.getName(), lookupElement.getLookupType(), lookupElement.getName());
            }
        }

        private void registerHints(RuntimeHints runtimeHints) {
            this.lookupElements.forEach(lookupElement -> registerProxyIfNecessary(runtimeHints, lookupElement.getDependencyDescriptor()));
        }

        private void registerProxyIfNecessary(RuntimeHints runtimeHints,
                                              DependencyDescriptor dependencyDescriptor) {
            if (this.candidateResolver != null) {
                Class<?> proxyClass = this.candidateResolver.getLazyResolutionProxyClass(dependencyDescriptor, null);
                if (proxyClass != null) {
                    ClassHintUtils.registerProxyIfNecessary(proxyClass, runtimeHints);
                }
            }
        }
    }
}
